package spindles.api.domain;

import static com.sleepycat.persist.model.DeleteAction.CASCADE;
import static com.sleepycat.persist.model.Relationship.MANY_TO_ONE;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.builder.CompareToBuilder;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import spindles.api.services.MatlabGateway;
import spindles.api.util.ApplicationException;
import spindles.api.util.ErrorMessages;
import spindles.api.util.Util;

import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.SecondaryKey;

@Entity
public class Epoch extends Interval{
	
	/**
	 * 
	 */
	private transient static final long serialVersionUID = 1737468081361628702L;

	transient final Logger log = LoggerFactory.getLogger(Epoch.class);
	
	public static final int LENGTH = 16; // seconds 	
	
	@SecondaryKey(relate=MANY_TO_ONE, relatedEntity=SessionPart.class,
			onRelatedEntityDelete=CASCADE)
	private long sessionPartID;
	
	
	private List<Double> eegSamples = 
		new ArrayList<Double>(LENGTH * SamplingRate.SR256.value());
	
	private List<Spindle> spindles = 
		new ArrayList<Spindle>();		
	
	private transient List<Double> SY = 
		new ArrayList<Double>();
	
	private transient Interval lastISI;
	
	public Epoch(Date from, Date to, long sessionPartID){
		super(from, to);
		this.sessionPartID = sessionPartID;
	}
	
	public Epoch(Date from, long sessionPartID){
		super(from, new Date(from.getTime() + LENGTH * 1000));
		this.sessionPartID = sessionPartID;
	}
	
	public Epoch(){
		super();
	}
	
	public int compareTo(Object o) {
		return CompareToBuilder.reflectionCompare(this, o, false, 
				Interval.class, new String[]{"eegSamples", "spindles", "isis"});
	}
	
	public boolean equals(Object obj) {
		return EqualsBuilder.reflectionEquals(this, obj, false, 
				Interval.class, new String[]{"eegSamples", "spindles", "isis"});
	}
	
	public int hashCode() {
		return HashCodeBuilder.reflectionHashCode(17, 
				37, this, false, Interval.class, 
				new String[]{"eegSamples", "spindles", "isis"});
	}	
	
	public void detectAndLoadSpindleIndications(SamplingRate sr, 
			ThresholdGroup group){
		if(isUsed(group)){
			throw new ApplicationException(ErrorMessages.DUPLICATE_THRESHOLDGROUP);
		}
		
		addSpindles(detectSpindles(mergePeaks(findPeaks(sr, group), group), group));
	}
	
	public boolean isUsed(ThresholdGroup group){
		return !getSpindleIndications(group).isEmpty();
	}
	
	public PerformanceEvaluation evaluatePerformance(ThresholdGroup gr, User user, 
			boolean softCriterion){
		
		PerformanceEvaluation pe = new PerformanceEvaluation();
		List<SpindleIndication> sis = getSpindleIndications(gr, softCriterion);
		List<VDSpindle> vds = getVDSpindles(user);		
		
		for(VDSpindle vdSpindle : vds){
			boolean matched = false;
			for(SpindleIndication si : sis){
				if(vdSpindle.intersect(si)){
					pe.incrementHits();
					sis.remove(si);
					matched = true;
					break;
				}
			}
			
			if(!matched) pe.incrementMisses();
		}
		
		for(SpindleIndication si : sis){
			boolean matched = false;
			for(VDSpindle vdSpindle : vds){
				if(vdSpindle.intersect(si)){
					pe.incrementFP2();
					matched = true;
					break;
				}
			}
			
			if(!matched) pe.incrementFP1();
		}		
		return pe;
	}
	
	public String toTSV(ThresholdGroup group, boolean soft){
		StringBuilder sb = new StringBuilder();
		List<SpindleIndication> sis = getSpindleIndications(group, soft);
	
		for (int i = 0; i < sis.size(); i++) {
			sb.append(printSpindle(sis.get(i)));
		}
		
		return sb.toString();
	}
	
	
	private double getRelativeStartTime(SpindleIndication si){
		return (si.getStart().getTime() - getStart().getTime())/1000.0;
	}
	
	private double getRelativeEndTime(SpindleIndication si){
		return (si.getEnd().getTime() - getStart().getTime())/1000.0;
	}
	
	
	private String printSpindle(SpindleIndication si){
		StringBuilder sb = new StringBuilder();		
		double dur = si.duration().doubleValue();		
		ISI isi = si.getIsi();
		
		sb.append(getStartTime() + "\t");
		sb.append(si.getStartTime()  + "\t");
		sb.append(getRelativeStartTime(si) + "\t");
		sb.append(si.getEndTime()  + "\t");
		sb.append(getRelativeEndTime(si) + "\t");
		sb.append(dur + "\t");		
		sb.append(isi != null ? isi.duration().doubleValue() : "-");
		sb.append(System.getProperty("line.separator"));
		return sb.toString();
	}
	
//	public void printSpindles(File file, List<SpindleIndication> spindles){
//		StringBuilder sb = new StringBuilder();
//		sb.append("\t\tSOFT CRITERION: START TIME: ");
//		sb.append(DateFormatUtils.ISO_TIME_NO_T_FORMAT.format(getStart()));
//		sb.append("\n\nSPINDLE OCCURENCE TIME\t");
//		sb.append("SPINDLE DURATION\t");
//		sb.append("SPINDLE NUMBER\n");
//		
//		//int counter = 0;
//		for(SpindleIndication si : spindles){
//			if(si.detectedWithSoftCriterion()){				
//				//sb.append(printSpindle(si, ++counter));
//			}			
//		}
//		
//		//counter = 0;
//		sb.append("\n\n\t\tHARD CRITERION: START TIME: ");
//		sb.append(DateFormatUtils.ISO_TIME_NO_T_FORMAT.format(getStart()));
//		sb.append("\n\nSPINDLE OCCURENCE TIME\t");
//		sb.append("SPINDLE DURATION\t");
//		sb.append("SPINDLE NUMBER\n");
//		
//		for(SpindleIndication si : spindles){
//			if(!si.detectedWithSoftCriterion()){				
//				//sb.append(printSpindle(si, ++counter));
//			}			
//		}
//		
//		try {
//			FileUtils.writeStringToFile(file, sb.toString());
//		} catch (IOException e) {
//			throw new ApplicationException(e);
//		}
//		
//	}
	
	public List<Interval> findPeaks(SamplingRate sr, ThresholdGroup group){ 
		if(SY.isEmpty()){
			SY = Arrays.asList(ArrayUtils.toObject(
					MatlabGateway.eegPeaks(ArrayUtils.toPrimitive(
					eegSamples.toArray(new Double[0])), sr)));
		}		
		
		List<Interval> result = new ArrayList<Interval>();
		for(int i = 0; i < SY.size(); i++){			
			if(SY.get(i) > group.getVtDouble()){
				int start = i;
				Interval inv = new Interval();
				inv.setStart(getTime(start, SamplingRate.SR128));				
				
				for( ;i < SY.size() && SY.get(i) > group.getVtDouble(); i++);
				int end = i - 1;
				inv.setEnd(getTime(end, SamplingRate.SR128));
				
				result.add(inv);
				log.info("Epoch {}: Peak {}: {}..{}", 
						new Object[]{this.getId(), result.size(), start + 1, end + 1});
			}			
		}
		
		return result;
	}
	
	public List<SpindleIndication> detectSpindles(List<Interval> mergedPeaks, 
			ThresholdGroup group){
		log.info("Detected {} spindles on epoch {}", mergedPeaks.size(), this.getId() );
		List<SpindleIndication> result = new ArrayList<SpindleIndication>();
		if(mergedPeaks.isEmpty()){
			return result;
		}
		for(Interval i : mergedPeaks){
			SpindleIndication si = new SpindleIndication(group, i.getStart(), i.getEnd(), true);
			si.setRelativeStartTime(getRelativeStartTime(si));
			si.setRelativeEndTime(getRelativeEndTime(si));			
			result.add(si);			
			
			if(i.duration().compareTo(group.getMinSpindleDuration()) > 0){
				SpindleIndication s = new SpindleIndication(group, i.getStart(), i.getEnd());
				s.setRelativeStartTime(getRelativeStartTime(s));
				s.setRelativeEndTime(getRelativeEndTime(s));	
				result.add(s);
			}
		}	
		return result;
	}
	
	public List<Interval> mergePeaks(List<Interval> peaks, ThresholdGroup group){
		List<Interval> result = new ArrayList<Interval>();
		
		if(peaks.isEmpty()){
			return result;
		}
		
		boolean lastMerged = false;		
		for (int i = 0; i < peaks.size() - 1; i++) {
			if (peaks.get(i).gap(peaks.get(i + 1)).compareTo(group.getTg()) < 0) {
				log.info("Merge candidate: peak {}", i + 1);
				if (!dropsLowerThanVg(new Interval(peaks.get(i).getEnd(), 
												   peaks.get(i + 1).getStart()), 
									  group.getVgDouble())) {
					// merge
					Interval merged = new Interval(peaks.get(i).getStart(),
							peaks.get(i + 1).getEnd());
					peaks.set(i, merged);
					peaks.remove(i + 1);
					
					log.info("Merged peaks {} and {}", i, i + 1);

					if (i == (peaks.size() - 1)) {
						lastMerged = true;
						break;
					}
					i--;
				} else {
					result.add(peaks.get(i));
				}
			} else {
				result.add(peaks.get(i));
			}
		}
		
		if(!lastMerged){
			result.add(peaks.get(peaks.size() - 1));
		}
		return result;
	}
	
	public boolean dropsLowerThanVg(Interval interval, double Vg){
		List<Double> samples = getSYSamples(interval);
		for(Double sample : samples){
			if(sample.doubleValue() < Vg){
				return true;
			}
		}
		return false;
	}
	
	public List<Double> getSYSamples(Interval interval){
		List<Double> result = new ArrayList<Double>();
		for(int i = 0; i < SY.size(); i++){
			Date currSample = getTime(i, SamplingRate.SR128);
			if((currSample.compareTo(interval.getStart()) >= 0)
			&& (currSample.compareTo(interval.getEnd()) <= 0)){
				result.add(SY.get(i));
			}
		}
		return result;
	}
	
	public void setSpindles(List<Spindle> spindles){
		this.spindles = spindles;
	}
	
	public void addSpindle(Spindle sp){
		spindles.add(sp);
	}
	
	public void addSpindles(List<? extends Spindle> spindles){
		this.spindles.addAll(spindles);
	}
	
	public List<VDSpindle> getVDSpindles(User user){
		List<VDSpindle> result = new ArrayList<VDSpindle>();
		for(Spindle s : spindles){
			if((s instanceof VDSpindle) && 
			  ((VDSpindle)s).getUserID() == user.getId()){
				result.add((VDSpindle)s);
			}
		}
		
		return result;
	}	
	
	public List<SpindleIndication> getSpindleIndications(ThresholdGroup group){
		List<SpindleIndication> result = new ArrayList<SpindleIndication>();
		for(Spindle s : spindles){
			if((s instanceof SpindleIndication) && 
			  ((SpindleIndication)s).getGroupID() == group.getId()){
				result.add((SpindleIndication)s);
			}
		}
		
		return result;
	}
	
	public List<SpindleIndication> getSpindleIndications(ThresholdGroup group, boolean soft){
		List<SpindleIndication> sis = getSpindleIndications(group);	
		List<SpindleIndication> result = new ArrayList<SpindleIndication>();
		
		for(SpindleIndication si : sis){
			if(si.detectedWithSoftCriterion() == soft){
				result.add(si);
			}
		}
		
		return result;
	}
	
	public Interval setISIs(ThresholdGroup group, boolean soft, Interval lastISI){
		this.lastISI = lastISI;		
		setISIs(getSpindleIndications(group, soft));		
		return getLastISI(group, soft);
	}
	
	public Interval setISIs(User user, Interval lastISI){
		this.lastISI = lastISI;
		setISIs(getVDSpindles(user));
		return getLastISI(user);
	}
	
	private void setISIs(List<? extends Spindle> sis) {
		for (int i = 0; i < sis.size(); i++) {
			Spindle si = sis.get(i);
			if (i == 0) {
				si.setIsi(lastISI == null ? null : new ISI(lastISI.getStart(),
						si.getStart()));
			} else {
				si.setIsi(new ISI(sis.get(i - 1).getStart(), si.getStart()));
			}
		}
	}
	
	private Interval getLastISI(List<? extends Spindle> sis){
		if(!sis.isEmpty()){
			return new Interval(sis.get(sis.size() - 1).getStart(), getEnd());
		} else if(lastISI == null){
			return null;
		} else{
			return new Interval(lastISI.getStart(), getEnd());
		}
	}
	
	private Interval getLastISI(ThresholdGroup group, boolean soft){
		return getLastISI(getSpindleIndications(group, soft));		
	}
	
	private Interval getLastISI(User user){
		return getLastISI(getVDSpindles(user));		
	}
	
	public double[][] getVDSpindlesForMatlab(User user){		
		return getSpindlesForMatlab(getVDSpindles(user));		
	}
	
	public double[][] getSpindleIndicationsForMatlab(ThresholdGroup group, boolean softCriterion){
		
		return getSpindlesForMatlab(getSpindleIndications(group, softCriterion));		
	}
	
	private double[][] getSpindlesForMatlab(List<? extends Spindle> spin){
		if(spin.isEmpty()){						
			return new double[][]{{}};
		}
		
		double[][] result = new double[spin.size()][2];
		
		for(int i = 0; i < spin.size(); i++){
			//start
			result[i][0] = Util.subtract(spin.get(i).getStart(),
										 this.getStart());
			//end
			result[i][1] = Util.subtract(spin.get(i).getEnd(),
										 this.getStart()) ;
		}
		return result;
	}
	

	public static int getSamplesCount(SamplingRate sfr){
		return sfr.value() * Epoch.LENGTH;
	}
	
	private Date getTime(int sampleNumber, SamplingRate sr){
		double temp = (sampleNumber + 1) * (1.0/sr.value());
		return Util.addSeconds(getStart(), temp);
	}
	
	
	public double[] getEEGSamples(){
		return ArrayUtils.toPrimitive(eegSamples.toArray(new Double[0]));
	}
	
	public double[] getEEGSamples(Date fromTime, Date toTime){
		double intervalMillis = (double)(LENGTH * 1000) / eegSamples.size();
		List<Double> intervalSamples = new ArrayList<Double>(2000);
		
		for(int i = 0; i < eegSamples.size(); i++){
			long sampleTime = Math.round(getStart().getTime() + i * intervalMillis);
			if ((fromTime.getTime() <= sampleTime)
					&& (toTime.getTime() >= sampleTime)) {
				intervalSamples.add(eegSamples.get(i));
			}
		}
		return ArrayUtils.toPrimitive(intervalSamples.toArray(new Double[0]));
	}	
	
	public double[] getFilteredEEGSamples(CutoffFrequency cut){
		if(cut == CutoffFrequency.ALL_FREQ){
			return getEEGSamples();
		}
		return MatlabGateway.filterEEG(getEEGSamples(), cut);
	}
	
	public void setEEGSamples(List<Double> samples){
		Validate.notNull(samples);
		eegSamples = samples;
	}
	
	public void addEEGSample(double sample){
		eegSamples.add(sample);
	}

	public long getSessionPartID() {
		return sessionPartID;
	}

	public void setSessionPartID(long sessionPartID) {
		this.sessionPartID = sessionPartID;
	}

}
